"""
Ftp_client v 0.4
Written by: Daniel Ortiz
Version .4: March 6 2009
Version .3: October 25 2007
"""

import wx
import wx.lib.flatnotebook as fnb
import wx.lib.mixins.listctrl as listmix
import wx.lib.agw.customtreectrl as TreeCtrl
import os
import ftplib
import threading
import time
import re
import socket

# Global symbol for FTP_SSL
FTP_SSL = True

try:
    import ssl
except ImportError:
    FTP_SSL = False
    pass
else:
    from ftplib import FTP, CRLF, Error
    from socket import _GLOBAL_DEFAULT_TIMEOUT
    
    class FTP_TLS(FTP):
        '''A FTP subclass which adds TLS support to FTP as described
        in RFC-4217.

        Connect as usual to port 21 securing control connection before
        authenticating.

        Securing data channel requires user to explicitly ask for it
        by calling prot_p() method.

        Usage example:
        >>> from ftplib import FTP_TLS
        >>> ftps = FTP_TLS('ftp.python.org')
        >>> ftps.login()  # login anonimously previously securing control channel
        '230 Guest login ok, access restrictions apply.'
        >>> ftps.prot_p()  # switch to secure data connection
        '200 Protection level set to P'
        >>> ftps.retrlines('LIST')  # list directory content securely
        total 9
        drwxr-xr-x   8 root     wheel        1024 Jan  3  1994 .
        drwxr-xr-x   8 root     wheel        1024 Jan  3  1994 ..
        drwxr-xr-x   2 root     wheel        1024 Jan  3  1994 bin
        drwxr-xr-x   2 root     wheel        1024 Jan  3  1994 etc
        d-wxrwxr-x   2 ftp      wheel        1024 Sep  5 13:43 incoming
        drwxr-xr-x   2 root     wheel        1024 Nov 17  1993 lib
        drwxr-xr-x   6 1094     wheel        1024 Sep 13 19:07 pub
        drwxr-xr-x   3 root     wheel        1024 Jan  3  1994 usr
        -rw-r--r--   1 root     root          312 Aug  1  1994 welcome.msg
        '226 Transfer complete.'
        >>> ftps.quit()
        '221 Goodbye.'
        >>>
        '''

        def __init__(self, host='', user='', passwd='', acct='', keyfile=None,
                     certfile=None, timeout=_GLOBAL_DEFAULT_TIMEOUT):
            self.keyfile = keyfile
            self.certfile = certfile
            self._prot_p = False
            FTP.__init__(self, host, user, passwd, acct, timeout)

        def login(self, user='', passwd='', acct='', secure=True):
            if secure:
                self.auth_tls()
            FTP.login(self, user, passwd, acct)

        def auth_tls(self):
            '''Set up secure control connection by using TLS.'''
            resp = self.voidcmd('AUTH TLS')
            self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile,
                                        ssl_version=ssl.PROTOCOL_TLSv1)
            self.file = self.sock.makefile(mode='rb')
            return resp

        def prot_p(self):
            '''Set up secure data connection.'''
            # PROT defines whether or not the data channel is to be protected.
            # Though RFC-2228 defines four possible protection levels,
            # RFC-4217 only recommends two, Clear and Private.
            # Clear (PROT C) means that no security is to be used on the
            # data-channel, Private (PROT P) means that the data-channel
            # should be protected by TLS.
            # PBSZ command MUST still be issued, but must have a parameter of
            # '0' to indicate that no buffering is taking place and the data
            # connection should not be encapsulated.
            self.voidcmd('PBSZ 0')
            resp = self.voidcmd('PROT P')
            self._prot_p = True
            return resp

        def prot_c(self):
            '''Set up clear text data channel.'''
            resp = self.voidcmd('PROT C')
            self._prot_p = False
            return resp

        # --- Overridden FTP methods

        def ntransfercmd(self, cmd, rest=None):
            conn, size = FTP.ntransfercmd(self, cmd, rest)
            if self._prot_p:
                conn = ssl.wrap_socket(conn, self.keyfile, self.certfile,
                                       ssl_version=ssl.PROTOCOL_TLSv1)
            return conn, size

        def retrbinary(self, cmd, callback, blocksize=8192, rest=None):
            self.voidcmd('TYPE I')
            conn = self.transfercmd(cmd, rest)
            while 1:
                data = conn.recv(blocksize)
                if not data:
                    break
                callback(data)
            # shutdown ssl layer
            if isinstance(conn, ssl.SSLSocket):
                conn.unwrap()
            conn.close()
            return self.voidresp()

        def retrlines(self, cmd, callback = None):
            if callback is None: callback = print_line
            resp = self.sendcmd('TYPE A')
            conn = self.transfercmd(cmd)
            fp = conn.makefile('rb')
            while 1:
                line = fp.readline()
                if self.debugging > 2: print '*retr*', repr(line)
                if not line:
                    break
                if line[-2:] == CRLF:
                    line = line[:-2]
                elif line[-1:] == '\n':
                    line = line[:-1]
                callback(line)
            # shutdown ssl layer
            if isinstance(conn, ssl.SSLSocket):
                conn.unwrap()
            fp.close()
            conn.close()
            return self.voidresp()

        def storbinary(self, cmd, fp, blocksize=8192, callback=None):
            self.voidcmd('TYPE I')
            conn = self.transfercmd(cmd)
            while 1:
                buf = fp.read(blocksize)
                if not buf: break
                conn.sendall(buf)
                if callback: callback(buf)
            # shutdown ssl layer
            if isinstance(conn, ssl.SSLSocket):
                conn.unwrap()
            conn.close()
            return self.voidresp()

        def storlines(self, cmd, fp, callback=None):
            self.voidcmd('TYPE A')
            conn = self.transfercmd(cmd)
            while 1:
                buf = fp.readline()
                if not buf: break
                if buf[-2:] != CRLF:
                    if buf[-1] in CRLF: buf = buf[:-1]
                    buf = buf + CRLF
                conn.sendall(buf)
                if callback: callback(buf)
            # shutdown ssl layer
            if isinstance(conn, ssl.SSLSocket):
                conn.unwrap()
            conn.close()
            return self.voidresp()

    all_errors = (Error, IOError, EOFError, ssl.SSLError)


class FTPThread(threading.Thread):
    """ A Thread which implements an ftplib client instance"""
    def __init__(self, window, selection):
        """Constructor for the FTP thread"""
        threading.Thread.__init__(self)
        self.window = window

        # Local variables
        self.list_dir = []
        self.initial = True
        self.transferring = False
        self.nonblocking_call = False
        self.logged_in = False
        self.is_dir = False
        self.exit_flag = False
        self.stop_flag = False
        self.get_file = ''
        self.ssl_connection = False
        self.transfer_file = ''
        self.working_directory = ''
        self.buffer_size = 4096
        self.update_cwd(selection)
        self.tab = None

        # Renaming
        self.old_name = ''
        self.new_name = ''

        # Deleting
        self.delete_file = ''

        # Filters
        self.filter = FilterString()

        # Status
        self.status = ''

        # Threading Events
        self.timeToQuit = threading.Event()
        self.activeThread = threading.Event()
        self.cwd_Event = threading.Event()
        self.send_Event = threading.Event()
        self.transfer_Event = threading.Event()
        self.name_Event = threading.Event()
        self.delete_Event = threading.Event()
        self.login_Event = threading.Event()
        self.status_interrupt = threading.Event()
        self.status_interrupt.clear()
        self.timeToQuit.clear()

    def set_activity(self, activity):
        """ Takes in a boolean variable activitiy to set the
        current active thread."""
        if activity:
            self.activeThread.set()
        else:
            self.activeThread.clear()

    def update_cwd(self, selection):
        """This function updates the location of the cwd"""
        if selection.find(':') != -1:
            last_directory = self.working_directory
            parsed = self.parse_root(selection)
            self.selection = parsed[0]
            self.working_directory = parsed[1]
        else:
            self.selection = selection
            self.working_directory = ''

    def update_tfile(self, ifile):
        """This function updates the file to be transferred from the
        FTP server to the client's computer."""
        self.get_file = ifile

    def update_sfile(self, ifile, idir):
        """This function updates the file to be sent to the FTP server."""
        self.transfer_file = ifile
        self.is_dir = idir

    def update_rfile(self, old_file, new_file):
        """This function takes in the file to be renamed (old_file), and its new name"""
        self.old_name = old_file
        self.new_name = new_file

    def update_dfile(self, dfile):
        """Update the file to be delete in this ftp instance"""
        self.delete_file = dfile

    def quit_FTPThread(self):
        """ Stops the thread"""
        self.timeToQuit.set()

    def get_root(self):
        """Returns the root of the ftp - 'ftp.ibiblio.org'"""
        return self.selection

    def set_filter(self, ifilter):
        """This function sets the threads filter to the parents."""
        self.filter.set_match(ifilter.base)

    def is_root(self):
        """This function returns True if the cwd is the root_dir"""
        return self.working_directory == self.root_dir or self.working_directory == ''

    def blocking_transfer(self):
        """If this FTP thread is in the middle of a blocking transfer, it should not
        be disturbed. A new thread will be created which handles the current query.
        It implies that this FTP thread will have siblings."""
        return self.transferring

    def parse_root(self, selection):
        """This function takes an ftp string, and parses the root from the cwd.
        E.g ftp.ibiblio.org:/random_dir/ => root = ftp.ibiblio.org, selection =
        random_dir"""
        try:
            split = selection.split(':')
            if len(split) != 2:
                raise TypeError('String fromat must be of type, "ftp.*.*:/cwd"')
            return split[0], split[1]
        except Exception, e:
            print 'Exception: %s' % (e,)

    def run(self):
        """ This function runs the FTP thread """
        while True:
            if self.initial:

                # Initialize thread
                wx.CallAfter(self.window.InitializeThread, self.selection, "Establishing FTP connection...")
                # Initial login
                try:
                    self.FTP = ftplib.FTP(self.selection)
                except socket.error, e:
                    # Recatch the socket connection refused error, and attempt to start FTP_TLS
                    if e.errno == 61 and FTP_SSL:
                        self.FTP = FTP_TLS(self.selection)
                        self.ssl_connection = True
                    else:
                        wx.CallAfter(self.window.PrintError, e)
                        wx.CallAfter(self.window.ThreadFinished, self, self.selection)
                        break
                except Exception, e:
                    wx.CallAfter(self.window.PrintError, e)
                    wx.CallAfter(self.window.ThreadFinished, self, self.selection)
                    break
        
                wx.CallAfter(self.window.UpdateStatus, self.selection, 'Logging in to FTP server...')
                # Login mechanism
                try:
                    # First try anonymous login, upon failure, open up
                    # the userpassframe
                    self.FTP.login()
                except:
                    self.window.OpenUserPassFrame(self)
                    # Blocking wait for login to complete
                    self.set_activity(False)
                    self.activeThread.wait()
                    if not self.logged_in:
                        break
                self.logged_in = True

                # Switch to secure data connection if ssl_connection is true
                if self.ssl_connection:
                    self.FTP.prot_p()               # Always attemp the P level of RFC 2228

                wx.CallAfter(self.window.UpdateStatus, self.selection, 'Uploading current working directory...')
                try:
                    self.FTP.cwd(self.working_directory)
                except:
                    pass
                self.FTP.dir(self.list_dir.append)
                self.root_dir = self.FTP.pwd()
                self.initial = False
                wx.CallAfter(self.window.ParseFTP, self)
            else:
                # Reset previous events
                self.transferring = False
                self.nonblocking_call = False
                self.stop_flag = False
                self.exit_flag = False
                self.cwd_Event.clear()
                self.send_Event.clear()
                self.transfer_Event.clear()
                self.name_Event.clear()
                self.delete_Event.clear()
                self.activeThread.clear()
                
                wx.CallAfter(self.window.UpdateStatus, self.selection, 'Commands complete: Idling')

                # Block waiting for new user input
                self.activeThread.wait()

                # Orderly exit
                if self.timeToQuit.isSet():
                    self.FTP.quit()
                    break

                # Changing the working directory
                if self.cwd_Event.isSet():
                    self.nonblocking_call = False
                    wx.CallAfter(self.window.UpdateStatus, self.selection, 'Changing to %s directory' % self.working_directory)
                    # Start at root 
                    try:
                        self.FTP.cwd(self.root_dir + self.working_directory)
                    except Exception, e:
                        wx.CallAfter(self.window.PrintError, e)
                        self.set_activity(False)
                        continue
                    self.list_dir = []
                    self.FTP.dir(self.list_dir.append)
                    wx.CallAfter(self.window.ParseFTP, self)

                # Transferring files from the server
                if self.send_Event.isSet():
                    self.transferring = True
                    self.nonblocking_call = True
                    self.tab = self.window.transfer_panel.SetTransferTab(self.get_file, self.selection)
                    if self.IsDir(self.get_file):
                        wx.CallAfter(self.window.UpdateStatus, self.selection, 'Downloading diretory: %s' % self.get_file)
                        self.RecursiveDownload(self.get_file, self.get_file)
                    else:
                        wx.CallAfter(self.window.UpdateStatus,self.selection, 'Downloading file: %s' % self.get_file)
                        self.DownloadFile()
                    self.window.transfer_panel.EndTransfer(self.selection)
                    wx.CallAfter(self.window.FinishDownload, self)

                # Transferring files to the server
                if self.transfer_Event.isSet():
                    self.transferring = True
                    self.nonblocking_call = True
                    self.tab = self.window.transfer_panel.SetTransferTab(os.path.basename(self.transfer_file), self.selection)
                    if self.is_dir:
                        wx.CallAfter(self.window.UpdateStatus, self.selection, 'Uploading directory: %s' % self.transfer_file)
                        self.RecursiveUpload(self.transfer_file)
                    else:
                        wx.CallAfter(self.window.UpdateStatus, self.selection, 'Uploading file: %s' % self.transfer_file)
                        self.UploadFile()
                    self.window.transfer_panel.EndTransfer(self.selection)
                    wx.CallAfter(self.window.FinishDownload, self)
                    
                    # Return to the current directory
                    self.FTP.cwd(self.root_dir + self.working_directory)
                    self.list_dir = []
                    self.FTP.dir(self.list_dir.append)
                    wx.CallAfter(self.window.ParseFTP, self)

                # Renaming a file on the server
                if self.name_Event.isSet():
                    self.nonblocking_call = False
                    wx.CallAfter(self.window.UpdateStatus, self.selection, 'Renaming %s to %s' % (self.old_name, self.new_name))
                    self.RenameFile(self.old_name, self.new_name)

                # Deleting a file on the server
                if self.delete_Event.isSet():
                    self.complete_delete = False
                    if self.DeleteFile(self.delete_file):
                        wx.CallAfter(self.window.UpdateAfterDelete, self)

                # Unorderly exit
                if self.exit_flag:
                    self.FTP.quit()
                    break
                    
                    
    def RecursiveDownload(self, serverpath, clientpath):
        """This function will recursively download directories or files.
        Great assistance came from Python Network Programming by John
        Goerzen 2004"""
        clientpath = os.path.abspath(clientpath)
        old_cdirectory = os.getcwd()
        if not os.path.isdir(clientpath):
            os.mkdir(clientpath)
        old_sdirectory = self.FTP.pwd()
        self.working_directory = old_sdirectory
        try:
            # Check for a peripheral interrupt
            if self.PeripheralInterrupt('Stop downloading %s?' % serverpath):
                return

            # Exit upon a peripheral interrupt from lower on the recursive stack
            if self.stop_flag or self.exit_flag:
                return
            os.chdir(clientpath)
            self.FTP.cwd(self.root_dir + self.working_directory + '/' + serverpath)
            self.working_directory = self.working_directory + '/' + serverpath
            list_cwd = self.FTP.nlst()
            if self.filter.isAll():
                list_of_files = list_cwd
            else:
                list_of_files = self.filter.compare_to_list(list_cwd)
            for sfile in list_of_files:
                if self.IsDir(sfile):
                    if sfile == '..' or sfile == '.':
                        continue
                    self.RecursiveDownload(sfile, sfile)
                else:
                    self.update_tfile(sfile)
                    self.DownloadFile()

                # Exit upon the interrupt
                if self.stop_flag or self.exit_flag:
                    break
        finally:
            os.chdir(old_cdirectory)
            if not os.listdir(clientpath):
                os.rmdir(clientpath)
            self.FTP.cwd(old_sdirectory)
            self.working_directory = old_sdirectory


    def RecursiveDelete(self, directory):
        """This function recursively deletes on an ftp server."""
        old_cwd = self.FTP.pwd()
        self.working_directory = old_cwd
        try:
            # Allow for peripheral interrupts
            if self.PeripheralInterrupt('Stop donwloading %s?' % self.get_file):
                return

            # Reentrant code should break on these flags
            if self.stop_flag and self.exit_flag:
                return
            
            self.FTP.cwd(self.root_dir + self.working_directory + '/' + directory)
            self.working_directory = self.working_directory + '/' + directory
            list_cwd = self.FTP.nlst()
            if self.filter.isAll():
                list_of_files = list_cwd
            else:
                list_of_files = self.filter.compare_to_list(list_cwd)
            for sfile in list_of_files:
                # Allow for peripheral interrupts in the current working directory
                if self.PeripheralInterrupt('Stop donwloading %s?' % self.get_file):
                    break
                if sfile == '..' or sfile == '.':
                    continue
                if self.IsDir(sfile):
                    self.RecursiveDelete(sfile)
                else:
                    # There could be permission problems
                    try:
                        self.FTP.delete(sfile)
                    except Exception, e:
                        self.window.PrintError(e)
                        break

                # Break out of child interrupts
                if self.stop_flag and self.exit_flag:
                    break
        finally:
            # Return to the previous directory
            self.FTP.cwd(old_cwd)
            self.working_directory = old_cwd
            
            # Remove the current directory if one has deleted all the files
            if len(list_of_files) == len(list_cwd):
                try:
                    self.FTP.rmd(directory)
                    self.complete_delete = True
                except:
                    self.complete_delete = False
            else:
                self.complete_delete = False

    def DownloadFile(self):
        """This function downloads the file or directory indicated"""
        self.FTP.voidcmd('TYPE I')
        try:
            dsocket, size = self.FTP.ntransfercmd('RETR %s' % self.get_file)
        except:
            return
        current_byte = 1
        self.window.transfer_panel.SetCurrentFile(self.get_file, self.tab)
        local_file = open(self.get_file, 'wb')
        while True:
            # Handles an interrupt from the status frame
            if self.PeripheralInterrupt('Stop donwloading %s?' % self.get_file):
                break
            self.window.transfer_panel.SetDeliveryStatus(current_byte, size, self.tab)
            temp_buf = dsocket.recv(4096)
            bytes = len(temp_buf)
            if not bytes:
                break
            local_file.write(temp_buf)
            current_byte += bytes

        if self.exit_flag or self.stop_flag:
            self.FTP.abort()
            self.FTP.voidresp()
        local_file.close()
        dsocket.close()
        if not (self.exit_flag or self.stop_flag):
            self.FTP.voidresp()
    

    def RecursiveUpload(self, clientpath):
        """This function uploads a local directory to the server."""
        for root, dirs, files in os.walk(clientpath):
            # Handle peripheral interrupts
            if self.PeripheralInterrupt('Stop uploading directory %s?' % clientpath):
                break
            
            # Try to make a new directory
            modified_root = ''
            path = ''
            list_files = self.filter.compare_to_list(files)
            if len(list_files):
                modified_root = root.replace(clientpath, '', 1)
                if self.filter.isAll():
                    if not self.MakeOneDirectory(clientpath, modified_root):
                        break
                else:
                    if not self.MakeManyDirectory(clientpath, modified_root):
                        break

                # Try changing into the new directory
                try:
                    self.FTP.cwd(self.path)
                except Exception, e:
                    self.window.PrintError(e)
                    return

                # Send the files in the directory
                for tfile in list_files:
                    self.transfer_file = os.path.abspath(root) + '/'+ tfile
                    self.UploadFile()
                    # Check in the inner loop as well
                    if self.stop_flag or self.exit_flag:
                        break

                # Peripheral interrupt has occured break
                if self.stop_flag or self.exit_flag:
                    break

    def MakeManyDirectory(self, clientpath, modified_root):
        """Makes several directories on the server as needed."""
        list_of_dirs = modified_root.split('/')
        cpath = os.path.basename(clientpath)
        current_dir = ''
        for ldir in list_of_dirs:
            if not ldir == '':
                current_dir = current_dir + '/%s' % ldir
            if not self.IsDir(cpath + current_dir):
                if not self.MakeOneDirectory(clientpath, current_dir):
                    return False
        return True
    def MakeOneDirectory(self, clientpath, modified_root):
        """Makes one directory on the FTP server"""
        try:
            self.path = self.root_dir + self.working_directory + os.path.basename(clientpath) +  modified_root
            self.FTP.mkd(self.path)
            return True
        except Exception, e:
            self.window.PrintError(e)
            return False

    def UploadFile(self):
        """This function uploads a file to the server."""
        try:
            fd = open(self.transfer_file, 'rb')
        except:
            return
        try:
            dsocket, size = self.FTP.ntransfercmd('STOR %s' % os.path.basename(self.transfer_file))
        except Exception, e:
            fd.close()
            print 'Failed %s' % os.path.basename(self.transfer_file), e
            return

        size = os.stat(self.transfer_file)[6]
        current_byte = 0
        self.window.transfer_panel.SetCurrentFile(os.path.basename(self.transfer_file), self.tab)
        while True:
            # Handles an interrupt from the status frame
            if self.PeripheralInterrupt('Stop uploading %s?' % self.transfer_file):
                break
            self.window.transfer_panel.SetDeliveryStatus(int(current_byte), int(size), self.tab)
            temp_buf = fd.read(4096)
            bytes = len(temp_buf)
            if not bytes:
                break
            dsocket.sendall(temp_buf)
            current_byte += bytes
        dsocket.close()
        fd.close()
        self.FTP.voidresp()
        
    def IsDir(self, sfile):
        """This function returns true if the file is a diretory"""
        if isinstance(sfile, basestring):
            try:
                if self.working_directory.endswith("/"):
                    self.FTP.cwd(self.root_dir + self.working_directory + sfile)
                else:
                    self.FTP.cwd(self.root_dir + self.working_directory + '/' + sfile)
                self.FTP.cwd(self.root_dir + self.working_directory)
                return True
            except Exception, e:
                return False
        else:
            return False

    def AttemptLogin(self, username, password):
        """This function attempts to log in a user with a certain password. Returns True
        if this process suceeds."""
        try:
            self.FTP = ftplib.FTP(self.selection)
            self.FTP.login(username, password)
            self.logged_in = True
            return True
        except Exception, e:
            self.window.PrintError(e)
            return False

    def RenameFile(self, oldfile, newfile):
        """This function renames the oldfile to the newfile on the server."""
        try:
            self.FTP.rename(oldfile, newfile)
        except Exception, e:
            self.window.PrintError(e)

    def DeleteFile(self, dfile):
        """This function deletes the dfile on the FTP server."""
        try:
            if self.IsDir(dfile):
                self.nonblocking_call = True
                self.status = 'Deleting directory: %s' % dfile
                wx.CallAfter(self.window.UpdateStatus, self.selection, self.status)
                self.RecursiveDelete(dfile)
            else:
                self.nonblocking_call = False
                self.status = 'Deleting file: %s' % dfile
                wx.CallAfter(self.window.UpdateStatus, self.selection, self.status)
                self.FTP.delete(dfile)
                self.complete_delete = True
            return True
        except Exception, e:
            self.window.PrintError(e)
            return False

    def PeripheralInterrupt(self, string):
        """This function handles an interrupt coming from the status frame"""
        if self.status_interrupt.isSet():
            # Query the user, block until user input
            self.status_interrupt.clear()
            self.QueryForExit(string)
            self.status_interrupt.wait()

            # Double clear so the loop can continue running
            self.status_interrupt.clear()

        # Orderly exit
        return self.exit_flag or self.stop_flag

    def QueryForExit(self, query_string):
        """This function asks the user for input on a ftp cancellation."""
        wx.CallAfter(self.window.QueryUser, query_string)

# Miscellaneous Classes
class FilterString:
    """Simpl class for filtering strings"""
    def __init__(self, base_string = ''):
        """Takes in a string for the base and compiles it
        into a regular expression"""
        self.set_base(base_string)

    def replace_wildcards(self, string):
        """This function replaces wildcards and periods to work with regular
        expression matching"""
        begin_string = []
        begin_string.append('^(')
        count = 0
        last_wildcard = False
        for char in string:
            if char == '*':
                if count:
                    begin_string.append(')(.*)')
                else:
                    begin_string.append('.*)')
                last_wildcard = True
            elif char == '.':
                begin_string.append('\.')
                last_wildcard = False
            else:
                begin_string.append(char)
                last_wildcard = False

            if last_wildcard:
                begin_string.append('(')

            count = count + 1
        if begin_string[len(begin_string) - 1] == ')':
            begin_string.append('$')
        else:
            begin_string.append(')$')
        return ''.join(begin_string)

    def isAll(self):
        """This function checks if the filter string is all inclusive"""
        return self.base == '(.*)'

    def set_base(self, string):
        """Set base to the a regular expression with wildcards."""
        if string:
            reg_string = self.replace_wildcards(string)
            self.base = re.compile(reg_string)
        else:
            self.base = re.compile('(.*)')

    def set_match(self, match):
        """This functions sets the regular expression to compare to"""
        self.base = match
            
    def compare_to_base(self, string):
        """This function compares a string to the basestring. True
        if it fits the base."""
        match = self.base.match(string)
        if match:
            return True
        else:
            return False

    def compare_to_list(self, ilist):
        """This function compares the base string to a list, and returns
        a list of items that passed a comparision with the base."""
        rlist = []
        for string in ilist:
            if isinstance(string, basestring):
                if self.compare_to_base(string):
                    rlist.append(string)
        return rlist

    def sift_files(self, column, cache, colnumber):
        """This function sifts the files in the cache by comparing to the column list.
           Afterwords, the filename is compared to the filter, and should it pass the
           filter it is appended to the ouput list"""
        slist = []
        for col in column:
            for row in cache:
                if row[colnumber] == col:
                    if isinstance(row[0], basestring):
                        if self.compare_to_base(row[0]):
                            slist.append(col)
                    cache.remove(row)
                    break
        return slist

class ListCache:
    """This simple class holds the current FTP directory in the ftp_tree
    format."""
    def __init__(self):
        """Instantiate cache"""
        self.cache = {}
        self.width = 4
        
    def add_file(self, filename, data):
        """Takes in the filename as the key, and list data holding the
        information about the file."""
        self.cache[filename] = data

    def remove_file(self, filename):
        """Takes in a filename and removes it from the dictionary"""
        try:
            del self.cache[filename]
        except:
            pass

    def get_filenames(self):
        """This function returns all of the filenames"""
        return self.cache.keys()

    def get_filter_files(self, ffiles):
        """Function takes in a list ffiles. The list is used to grab the
        relevant files"""
        ofiles = []
        for filename, data in self.cache.iteritems():
            if filename in ffiles:
                ofiles.append([filename, data[0], data[1], data[2]])
        return ofiles

    def get_all_files(self):
        """Function returns all files in a list that can be easily appended
        to the ListCtrl."""
        ofiles = []
        for filename, data in self.cache.iteritems():
            ofiles.append([filename, data[0], data[1], data[2]])
        return ofiles

    def get_column(self, num):
        """This function returns a column of the ListCtrl"""
        if not num in range(self.width):
            raise ValueError('The argument num %d is out of range' % num)

        # If >1, in values, else return keys
        if num:
            ocolumns = []
            for filename, data in self.cache.iteritems():
                ocolumns.append(data[num - 1])
            return ocolumns
        else:
            return self.cache.keys()

    def get_order_files(self, column, colnumber):
        """Given an ordered column return the cache ordered"""
        olist = []
        all_files = self.get_all_files()
        for col in column:
            for data in all_files:
                if data[colnumber] == col:
                    olist.append(data)
                    all_files.remove(data)
                    break
        return olist

    def clear(self):
        """This function clears the cache. Works by simply reinitializing cache"""
        self.cache = {}
    

# wx Classes

# Modified list ctrl
class ResizableListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
    def __init__(self, parent, ID, pos = wx.DefaultPosition, size = wx.DefaultSize, style = 0):
        wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
        listmix.ListCtrlAutoWidthMixin.__init__(self)

class HorizontalResizableMixin:
    def __init__(self, offset, debug, name = ''):
        """
        Initialize the size events of the master class
        offset - an integer value to subtract from the horizontal width
        debug - A flag to debug on resize
        """
        self.offset = offset
        self.debug = debug
        self.Bind(wx.EVT_SIZE, self._onResize)

    def _onResize(self, event):
        """Respond to the notebook being resized"""
        if 'gtk2' in wx.PlatformInfo:
            self._doResize()
        else:
            wx.CallAfter(self._doResize)
        event.Skip()

    def _doResize(self):
        """
        Resize the notebook to the top level parent width appropriate width
        """
        size = self.GetTopLevelParent().GetSize()[0] - self.offset
        if self.debug:
            print "Size", size
        self.SetSize((size, -1))

class ResizableNotebook(wx.Notebook, HorizontalResizableMixin):
    def __init__(self, parent, ID, pos = wx.DefaultPosition, size = wx.DefaultSize, style = 0, offset = 0, debug = False):
        wx.Notebook.__init__(self, parent, ID, pos, size, style)
        HorizontalResizableMixin.__init__(self, offset, debug)

class ResizableGauge(wx.Gauge, HorizontalResizableMixin):
    def __init__(self, parent, ID, grange = 100, pos = wx.DefaultPosition, size = wx.DefaultSize,
                 style = 0, validator = wx.DefaultValidator, name = wx.GaugeNameStr, offset = 0, debug = False):
        wx.Gauge.__init__(self, parent, ID, grange, pos, size, style, validator, name)
        HorizontalResizableMixin.__init__(self, offset, debug)

class ResizableTextCtrl(wx.TextCtrl,  HorizontalResizableMixin):
    def __init__(self, parent, id = -1, value = wx.EmptyString, pos = wx.DefaultPosition, size = wx.DefaultSize,
                 style = 0, validator = wx.DefaultValidator, name = wx.TextCtrlNameStr, offset = 0, debug = False):
        wx.TextCtrl.__init__(self, parent, id, value, pos, size, style, validator, name)
        HorizontalResizableMixin.__init__(self, offset, debug)


# Frames and Panels
class UserPassFrame(wx.Frame):
    """
    A frame which contains the textctrl to input the
    name and password
    """
    def __init__(self, parent, thread):
        """
        The constructor of the UserPass Frame
        """
        size = (200, 100)
        dis = size[0]/15
        self.thread = thread
        wx.Frame.__init__(self, parent, -1, 'Login:', size = size)
        panel = wx.Panel(self, -1)

        # User input box
        userLabel = wx.StaticText(panel, -1, 'User:')
        self.userText = wx.TextCtrl(panel, -1, '', size = (size[0] / 2, -1))
        self.userText.SetInsertionPoint(0)

        # Password input box
        passLabel = wx.StaticText(panel, -1, 'Password:')
        self.passText = wx.TextCtrl(panel, -1, '', size = (size[0]/2, -1),
                                    style = wx.TE_PASSWORD)

        # Buttons
        self.cancelButton = wx.Button(panel, label = 'Cancel', size = (size[0]/2 - dis, -1))
        self.Bind(wx.EVT_BUTTON, self.OnCancel, self.cancelButton)
        self.okButton = wx.Button(panel, label = 'Ok', size = (size[0]/2 - dis, -1))
        self.Bind(wx.EVT_BUTTON, self.OnOk, self.okButton)

        # Add a sizer
        self.sizer = wx.FlexGridSizer(cols = 2, hgap = 6, vgap = 6)
        self.sizer.AddMany([userLabel, self.userText, passLabel, self.passText,
                       self.cancelButton, self.okButton])
        panel.SetSizer(self.sizer)


    def OnCancel(self, event):
        """
        If the user cancels, just exit
        """
        self.Close()

    def OnOk(self, event):
        """
        Validate the password/user pair. If valid, set the parent frame state variables.
        If not, render the window telling the user that the login failed.
        """
        self.password = self.passText.GetValue()
        self.username = self.userText.GetValue()
        if self.thread.AttemptLogin(self.username, self.password):
            self.thread.set_activity(True)
            self.Close()
        else:
            self.SetTitle('Login: Failed')

class StatusPanel(wx.Panel):
    """This frame display the status of all the FTP threads, and allows for
    thread by thread manipulation"""
    def __init__(self, parent, main_frame, isize, threads = None):
        """
        parent - the wx window class which is the parent of the panel
        main_frame - the top level window of the application
        isize - the dimension of the panel
        threads - an array of threads to potentially add to the status panel
        """

        # The default constructor
        wx.Panel.__init__(self, parent, -1)

        self.size = isize
        self.parent = parent
        self.window = main_frame
        self.slist_item = None
        
        # Update the position
        point = self.GetPosition()
        point.y = point.y + 75
        self.SetPosition(point)
        
        # The threads to be querying
        if threads:
            self.threads = threads
        else:
            self.threads = []

        # Create the objects that exist in this frame
        self.CreateMainSizer()

        # Resize the notebook as the frame expands
        self.Bind(wx.EVT_SIZE, self.OnSize)

    def CreateMainSizer(self):
        """This function creates a listctrl that visualizes the current threads"""

        # Create the relevant sizer
        sizer = wx.BoxSizer(wx.VERTICAL)

        # Create the listCtrl
        self.status_tree = ResizableListCtrl(self, -1, style = wx.LC_REPORT)
        self.status_tree.Bind(wx.EVT_LIST_KEY_DOWN, self.OnListKeyDown)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, self.status_tree)
        self.names = ['Thread', 'Status']
        for col, text in enumerate(self.names):
            self.status_tree.InsertColumn(col, text, width = 2 * self.size[1] / 5)

        # Populate the panel
        sizer.Add(self.status_tree, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetAutoLayout(True)

        # Initialize the threads
        if self.threads:
            self.InitializeThreads(self.threads)

    def OnSize(self, event):
        """Update the width of the notebook"""
        self.SetSize(self.parent.GetSize())

    def InitializeThreads(self, threads):
        """This function initializes all of the threads"""
        for thread in self.threads:
            self.InitializeThread(threads.selection, threads.status)

    def InitializeThread(self, root, status):
        """This function initializes one thread, and adds it to the ListCtrl"""
        list_status = [root, status]
        self.status_tree.Append(list_status)

    def RemoveThreadSelection(self, root):
        """Remove the thread from the listctrl"""
        for num in range(self.status_tree.GetItemCount()):
            if self.status_tree.GetItemText(num) == root:
                self.status_tree.DeleteItem(num)

    def RemoveThread(self):
        """This function removes the thread that corresponds with the last deleted item"""
        self.status_tree.DeleteItem(self.slist_item)

    def UpdateStatus(self, root, istatus):
        """This function updates the status of a thread"""
        if not istatus:
            return
        for num in range(self.status_tree.GetItemCount()):
            if self.status_tree.GetItemText(num) == root:
                self.status_tree.SetStringItem(num, 1, istatus)

    def OnItemSelected(self, event):
        """This function it stores the current selection"""
        if self.slist_item == event.m_itemIndex:
            tobject = self.status_tree.GetItem(self.slist_item, 0)
            self.currentSelection = tobject.GetText()
        else:
            self.slist_item = event.m_itemIndex
            tobject = self.status_tree.GetItem(self.slist_item, 0)
            self.currentSelection = tobject.GetText()
            self.window.SwitchThreads(self.currentSelection)
            

    def OnListKeyDown(self, event):
        """Checks if the user presses delete. If the thread is busy, interrupt, and
        query the user if he wants to discontinue the operation. If the thread is
        not busy, it merely deletes the thread"""
        if event.GetKeyCode() == 8 or event.GetKeyCode() == wx.WXK_DELETE:
            self.current_thread = self.window.FindRelevantThread(self.currentSelection)
            if not self.current_thread.activeThread.isSet():
                # If idling simply exit
                self.current_thread.timeToQuit.set()
                self.current_thread.set_activity(True)
                self.RemoveThread()
                self.window.RemoveThread(self.current_thread)
            elif self.current_thread.nonblocking_call:
                # Interrupt recursive operation
                self.current_thread.status_interrupt.set()
            else:
                # Force delete
                self.current_thread.exit_flag = True
                self.RemoveThread()
                self.window.RemoveThread(self.current_thread)

    def QueryUser(self, string):
        """This function queries the user to stop a download"""
        dlg = wx.MessageDialog(None, string, 'Want to Quit?')
        if dlg.ShowModal() == wx.ID_OK:
            self.current_thread.stop_flag = True
        dlg.Destroy()
        # Restart thread
        self.current_thread.status_interrupt.set()

class TransferPanel(wx.Panel):
    """Status bar for transferring data"""
    def __init__(self, parent,  ftw = False):
        """Constructor for StatusBar takes in the parent window"""
        wx.Panel.__init__(self, parent, -1)

        # The transfer frame can exist in two forms. One where is full, and
        # the other is simply the gauge. The full has more detailed control
        # embedded directly in the frame. The other frame merely prints out
        # the status of the transfer
        self.full_transfer_window = ftw

        # The tabs
        self.tab_names = []
        self.tab_count = 0
        self.stress_count = 0
        self.stress_constant = 100

        # Range of the gauge
        self.range = 100
        self.current_pos = 0
        self.main_size = None

        # The widgets
        self.gauges = []
        self.files = []
        self.status = []
        self.panels = []
        self.threads = []

        # Actual Subwidgets of Status bars
        self.DrawStatusTransferFrame()

    def DrawStatusTransferFrame(self):
        """This function draws the status transfer frame. This frame simply contains
        a gauge, and two textctrl's which show the size and name of the file being
        transferred."""
        # Create the relevant box sizer
        sizer = wx.BoxSizer(wx.EXPAND)

        # The flat notebook
        self.book = ResizableNotebook(self, -1 )
        self.tab_names.append('')
        self.AddTab()

        # Populate the notebook
        sizer.Add(self.book, 1, wx.EXPAND)
        self.SetSizer(sizer)
        #self.SetAutoLayout(True)

    def AddTab(self):
        """This funcion adds a transfer tab to the notebook"""
        panel = wx.Panel(self.book, -1)
        transfer_file = wx.TextCtrl(panel, -1, style = wx.TE_READONLY)

        # Resizable gauge
        gaugeoffset = 200
        gauge = ResizableGauge(panel, -1, self.range, size = (self.GetParent().GetSize()[0] - gaugeoffset, 40), offset = gaugeoffset)
        tstatus = wx.TextCtrl(panel, -1, style = wx.TE_READONLY)
        self.panels.append(panel)
        self.files.append(transfer_file)
        self.gauges.append(gauge)
        self.status.append(tstatus)
        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        main_sizer.Add(transfer_file)
        main_sizer.Add(gauge, 1)
        main_sizer.Add(tstatus)
        panel.SetSizer(main_sizer)
        panel.SetAutoLayout(True)
        self.book.InsertPage(self.tab_count, panel, ' ', True, -1)

    def SetTransferTab(self, istr, selection):
        """This function sets the tab name for the current transfer"""
        self.threads.append(selection)
        if self.tab_count:
            self.AddTab()
        self.book.SetPageText(self.tab_count, istr)
        self.tab_count = self.tab_count + 1
        return self.tab_count - 1

    def EndTransfer(self, selection):
        """This function ends the transfer from this particular thread"""
        count = 0
        for sel in self.threads:
            if sel == selection:
                break
            count = count + 1

        if self.book.GetPageCount() == 1:
            self.tab_count = self.tab_count - 1
            self.status[count].SetValue('')
            self.gauges[count].SetValue(0)
            self.files[count].SetValue('')
            self.book.SetPageText(self.tab_count, '')
            self.threads[count] = ''
        else:
            # Remove the values
            self.status.pop(count)
            self.gauges.pop(count)
            self.files.pop(count)
            panel = self.panels.pop(count)
            self.threads.pop(count)

            # Decrement the total count
            self.tab_count = self.tab_count - 1

            # Remove the page
            self.book.RemovePage(panel)

        
    def SetCurrentFile(self, istr, tab):
        """This function takes in a string argument and displays it
        as the first element of the status bar with some formatting."""
        if isinstance(istr, basestring):
            self.files[tab].SetValue('File: %s' % istr)
            
    def SetDeliveryStatus(self, currentByte, totalSize, tab):
        """This function sets the string which is the status of the
        the transferring file."""
        if isinstance(currentByte, int) and isinstance(totalSize, int):
            SizeString = self.StringFileSize(totalSize)
            self.status[tab].SetValue(SizeString)
            self.updateGauge(currentByte, totalSize, tab)
        elif isinstance(currentByte, int) and not totalSize:
            # Server does not return the size of the file with ntransfercmd
            # This hack makes sure that the window is not painted excessively
            # causing Kernel Panic.
            self.stress_count += 1
            if self.stress_count % self.stress_constant == 0:
                SizeString = self.StringFileSize(currentByte)
                self.status[tab].SetValue(SizeString)
            
    def StringFileSize(self, totalSize):
        """Function takes in an integer and prettifies it into a string"""
        if totalSize > 1000000000:
            SizeString = '%.1f gB' % (float(totalSize)/1000000000.0)
        elif totalSize > 1000000:
            SizeString = "%.1f mB" % (float(totalSize)/1000000.0)
        elif totalSize > 1000:
            SizeString = "%.1f kB" % (float(totalSize)/1000.0)
        else:
            SizeString = "%d B" % totalSize

        return SizeString

    def updateGauge(self, currentByte, totalSize, tab):
        """This function updates the gauge according to the amount of
        bytes transferred."""
        if totalSize:
            percent = 100.0 * float(currentByte) / float(totalSize)
            if percent >= 100.0:
                self.gauges[tab].SetValue(100)
            else:
                self.gauges[tab].SetValue(int(percent))
        else:
            self.gauges[tab].SetValue(100)


    def OnSearchButton(self, event):
        """This function creates open up a file/open widget to search local
        machine for the directory to transfer or receive into"""
        pass

        
class FTPFrame(wx.Frame):
    """FTP frame"""
    def __init__(self, size = (825, 600)):
        wx.Frame.__init__(self, None, -1, size = size, title = 'FTP Client',
                          style = wx.DEFAULT_FRAME_STYLE)
        self.threads = []
        self.size = size
        self.CreateMainSizer()
        self.CreateMenu()
    
        # FTP variables
        # Welcome is a string which holds the welcome script printed by
        # the FTP server.
        self.welcome = ''
        self.isFile = None
        self.directory = os.getcwd()
        self.last_cwd = ''

        # Frame variables
        self.control_frame = None
        self.EditingList = False
        self.ItemSelected = False

        # Filter variables
        self.filter = FilterString()
        self.OnFilterHide = False
        self.Order = False
        self.LastColumn = 0

        # Cache variables
        self.list_cache = ListCache()
            
        # Global Events
        self.Bind(wx.EVT_CLOSE, self.OnClose)

    def OnClose(self, event):
        """This function stops all of the threads, and closes the wx frame"""
        self.StopThreads()
        self.Destroy()
        
    def CreateMainSizer(self):
        """This function creates the main sizer. The main sizer contains
        the components for basic FTP transfer"""
        # The main list ctrl
        self.ftp_tree = ResizableListCtrl(self, -1, style = wx.LC_REPORT | wx.LC_EDIT_LABELS)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, self.ftp_tree)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnItemDeselected, self.ftp_tree)
        self.ftp_tree.Bind(wx.EVT_LEFT_DCLICK, self.OnListDoubleClick)
        self.ftp_tree.Bind(wx.EVT_RIGHT_DOWN, self.OnListRightDown)
        self.ftp_tree.Bind(wx.EVT_LIST_END_LABEL_EDIT, self.OnListEdit)
        self.ftp_tree.Bind(wx.EVT_LIST_BEGIN_LABEL_EDIT, self.OnBeginListEdit)
        self.ftp_tree.Bind(wx.EVT_LIST_KEY_DOWN, self.OnListKeyDown)
        self.ftp_tree.Bind(wx.EVT_LIST_COL_CLICK, self.OnListColumnClick)

        
        self.ftp_columns = ['Filename', 'Size', 'Date', "Type"]
        for col, text in enumerate(self.ftp_columns):
            if isinstance(text, basestring):
                self.ftp_tree.InsertColumn(col, text, width = self.size[0] / 4)
            else:
                self.ftp_tree.InsertColumn(col, "")
                self.ftp_tree.SetColumnImage(col, text)

        # Create the text Ctrl
        toolbarPanel = wx.Panel(self, -1)
        browse_offset = 350
        search_offset = 655
        browse_ftp_text = wx.StaticText(toolbarPanel, -1, "Ftp:")

        # TextCtrl's to process user inputs
        self.browse_ftp = wx.TextCtrl(toolbarPanel, -1, size = (self.GetSize()[0] - browse_offset, -1),
                                      style = wx.TE_PROCESS_ENTER)
        self.browse_ftp.Bind(wx.EVT_TEXT_ENTER, self.CheckForFTPEntry)
        search_ftp_text = wx.StaticText(toolbarPanel, -1, "Control:")
        self.search_ftp = wx.TextCtrl(toolbarPanel, -1, size = (self.GetSize()[0] - search_offset, -1),
                                      style = wx.TE_PROCESS_ENTER)
        self.search_ftp.Bind(wx.EVT_TEXT_ENTER, self.SearchFTPResults)

        # Create the toolbar sizer to layout "hand-drawn" toolbar
        toolbarSizer = wx.BoxSizer(wx.HORIZONTAL)
        toolbarSizer.Add(browse_ftp_text)
        toolbarSizer.Add(self.browse_ftp,3, flag = wx.EXPAND)
        toolbarSizer.Add(search_ftp_text, flag = wx.EXPAND)
        toolbarSizer.Add(self.search_ftp, 1, flag = wx.EXPAND)
        toolbarPanel.SetSizer(toolbarSizer)

        # Create the Notebook which contains the relevant status panels
        self.status_notebook = ResizableNotebook(self, -1, style = wx.BK_DEFAULT, size = (self.size[0], -1))
        self.transfer_panel = TransferPanel(self.status_notebook)
        self.status_frame = StatusPanel(self.status_notebook, self, self.GetSize())
        self.status_notebook.AddPage(self.transfer_panel, "Transfer")
        self.status_notebook.AddPage(self.status_frame, "Status")

        # Create the main sizer
        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
        self.main_sizer.Add(toolbarPanel, flag = wx.EXPAND)
        self.main_sizer.Add(self.ftp_tree, 2, flag = wx.EXPAND)
        self.main_sizer.Add(self.status_notebook, 1, flag = wx.EXPAND)
        self.SetSizer(self.main_sizer)


    def CreateMenu(self):
        """This function creates the menu for the FTPFrame. """

        # The Window Menu
        self.window_menu = wx.Menu()
        tframe = self.window_menu.Append(-1, 'Transfer Window\tCtrl-t')
        self.Bind(wx.EVT_MENU, self.ShowTransferFrame, tframe)
        sframe = self.window_menu.Append(-1, 'Status Window\tCtrl-s')
        self.Bind(wx.EVT_MENU, self.ShowStatusFrame, sframe)

        # The Control Menu
        self.control_menu = wx.Menu()
        hide = self.control_menu.Append(-1, 'Hide\tCtrl-h')
        self.Bind(wx.EVT_MENU, self.OnHide, hide)

        # Global Menubar
        self.ftp_menubar = wx.MenuBar()
        self.ftp_menubar.Append(self.window_menu, 'Frames')
        self.ftp_menubar.Append(self.control_menu, 'Control')
        self.SetMenuBar(self.ftp_menubar)

    def CheckForFTPEntry(self, event):
        """
        Upon pressing enter in the browse_ftp text control, a thread is
        created which will grab the files from the ftp root.
        """
        selection = self.browse_ftp.GetValue()
        if not selection.find('ftp://'):
            selection = selection[len('ftp://'):]
        newThread = True
        oldThread = None
        for thread in self.threads:
            try:
                root = thread.get_root()
                if selection.find(root) != -1:
                    newThread = False
                    oldThread = thread
                    break
            except:
                continue
        if newThread:
            # Initialize an ftp thread
            thread = FTPThread(self, selection)
            thread.set_filter(self.filter)
            thread.set_activity(True)
            self.threads.append(thread)
            thread.start()
        else:
            self.last_cwd = oldThread.selection + ':' + oldThread.working_directory
            oldThread.timeToQuit.clear()
            oldThread.update_cwd(selection)
            oldThread.cwd_Event.set()
            oldThread.set_activity(True)
                
            
    def SearchFTPResults(self, event):
        """
        This text ctrl searches for the user's queries, in all of the ftp
        threads
        """
        input_string = self.search_ftp.GetValue()
        if input_string == '':
            input_string = '*'

        self.filter.set_base(input_string)
        if self.Order:
            self.SortColumnAndUpdate(self.LastColumn)
        else:
            list_of_filenames = self.GetListOfFilenames()        
            if list_of_filenames:
                new_files = self.filter.compare_to_list(list_of_filenames)
                if not self.OnFilterHide:
                    self.FilterUpdateList(new_files)
                else:
                    self.FilterUpdateList(list_of_filenames)
                        
    def ThreadFinished(self, thread, selection):
        """This function removes a thread from the internal thread list"""
        # Shutdown thread
        thread.quit_FTPThread()
        self.threads.remove(thread)

        # Remove visualization of threads
        self.status_frame.RemoveThreadSelection(selection)

    def StopThreads(self):
        """Remove all threads from the thread pool"""
        while self.threads:
            thread = self.threads[0]
            if isinstance(thread, FTPThread):
                thread.quit_FTPThread()
                thread.set_activity(True)

                # Join the thread to the main thread
                thread.join(.5)
                if thread.isAlive():
                    raise RuntimeError, "Thread was not destroyed upon exit, a timeout occured after .5 seconds"
            self.threads.remove(thread)

    def ParseFTP(self, fthread):
        """This function takes in the FTPthread, parses the information glead from the
        server, and updates the user interface accordingly"""
        # Block FTP Thread
        fthread.set_activity(False)
        self.files_in_cwd = fthread.list_dir
        is_root = fthread.is_root()
        self.UpdateFTPList(self.files_in_cwd, is_root)
        
    def UpdateFTPList(self, cwd, is_root):
        """This function updates the listctrl, printing the cwd of the ftp file. Currently only
        works for UNIX systems"""
        self.list_cache.clear()
        self.ftp_tree.DeleteAllItems()
        if not is_root:
            parent_list = ['Parent', '', '', 'Directory']
            self.list_cache.add_file(parent_list[0], parent_list[1:])
        for listing in cwd:
            append_list = []
            split_list = listing.split()
            append_list.append(' '.join(split_list[8:]))
            append_list.append(split_list[4] + ' bytes')
            date = split_list[6]
            if len(date) == 1:
                date = '0' + date
            append_list.append(split_list[5] + ' ' + date + ' ' + split_list[7])
           
            
            if listing.startswith('d'):
                append_list.append('Directory')
            elif listing.startswith('l'):
                append_list.append('Link')
            elif listing.startswith('b'):
                append_list.append('Block Device')
            elif listing.startswith('c'):
                append_list.append('Character Device')
            else:
                append_list.append('File')

            if append_list[0] == '.' or append_list[0] == '..':
                continue
            self.list_cache.add_file(append_list[0], append_list[1:])


        if self.Order:
            self.SortColumnAndUpdate(self.LastColumn)
        else:
            list_of_filenames = self.GetListOfFilenames()        
            if list_of_filenames:
                new_files = self.filter.compare_to_list(list_of_filenames)
                self.FilterUpdateList(new_files)

    def PrintError(self, error):
        """This function prints the FTP error in a wx MessageBox"""
        dlg = wx.MessageDialog(self, str(error), 'FTP Error', style = wx.OK)
        result = dlg.ShowModal()
        dlg.Destroy()

        # Return to previous state
        if self.last_cwd:
            self.browse_ftp.SetValue(self.last_cwd)

    def OnItemSelected(self, event):
        """This function is triggered upon an item in the current listctrl being selected.
        It saves this item, for later commands."""
        self.ItemSelected = True
        self.slist_item = event.m_itemIndex
        tobject = self.ftp_tree.GetItem(self.slist_item, 0)
        dobject = self.ftp_tree.GetItem(self.slist_item, 3)
        self.isFile = 'Directory' == dobject.GetText() or 'Link' == dobject.GetText()
        self.currentFile = tobject.GetText()

    def OnItemDeselected(self, event):
        """Upon deselection update the list ctrl selection state variables"""
        self.ItemSelected = False

    def OnListDoubleClick(self, event):
        """This function gets the current selections filename, and changes
        directory accordingly."""
        if self.isFile:
            selection = self.browse_ftp.GetValue()
            which_thread = self.FindRelevantThread()
            if which_thread:
                # Minor formatting issues with selection
                if selection.find(':') == -1:
                    selection = selection + ':'
                length_selection = len(selection)
                precursor = '/'
                if selection.endswith('/'):
                    precursor = ''

                # Traversal of FTP directory, and save session information
                self.last_cwd = which_thread.selection + ':' + which_thread.working_directory
                if self.currentFile == 'Parent':
                    index = selection.rfind('/')
                    if index == -1:
                        self.UpdateListCwdToThread(which_thread, which_thread.selection + ':')
                    else:
                        self.UpdateListCwdToThread(which_thread, selection[:index])
                else:
                    self.UpdateListCwdToThread(which_thread, selection + '%s%s' % (precursor, self.currentFile))

    def OnListEdit(self, event):
        """This function allows the editing of the list ctrl"""
        self.EditingList = False
        if event.IsEditCancelled() or event.GetLabel() == '':
            event.Veto()
            return
        else:
            thread = self.FindRelevantThread()
            if thread:
                # Check if the user changed. Saves time!
                if self.currentFile != event.GetLabel():
                    thread.update_rfile(self.currentFile, event.GetLabel())
                    thread.name_Event.set()
                    thread.set_activity(True)
        

    def OnBeginListEdit(self, event):
        """This function sets the edit listctrl state variable."""
        self.EditingList = True

    def OnListKeyDown(self, event):
        """Function triggered when a key is pressed in the ListCtrl"""
        if event.GetKeyCode() == 8 or event.GetKeyCode() == wx.WXK_DELETE:
            if not self.EditingList and self.ItemSelected:
                thread = self.FindRelevantThread()
                if thread:
                    thread.update_dfile(self.currentFile)
                    thread.set_filter(self.filter)
                    thread.delete_Event.set()
                    thread.set_activity(True)

    def OnListColumnClick(self, event):
        """If the user presses the column it sorts it. Repeated clicking will
        change the sorting from ascending to descending and vice versa"""
        colnumber = event.GetColumn()
        self.LastColumn = colnumber
        self.Order = True
        self.SortColumnAndUpdate(self.LastColumn)

    def SortColumnAndUpdate(self, colnumber):
        """This function sorts a column and updates the list ctrl"""
        column = self.list_cache.get_column(colnumber)
        
        if colnumber == 2:
            # Sort the dates
            n2date = self.SortDates(column)
                
            if not self.OnFilterHide:
                new_column = self.filter.sift_files(n2date, self.list_cache.get_all_files(), colnumber)
                self.FilterOrderUpdateList(new_column)
            else:
                self.FilterOrderUpdateList(n2date)
                
        elif colnumber == 1:
            ncolumn = []
            for col in column:
                ncolumn.append(int(col.split(' ')[0]))
            ncolumn.sort()
            count = 0
            for ncol in ncolumn:
                ncolumn[count] = '%d bytes' % ncol
                count = count + 1
            
            if not self.OnFilterHide:
                new_column = self.filter.sift_files(ncolumn, self.list_cache.get_all_files(), colnumber)
                self.FilterOrderUpdateList(new_column)
            else:
                self.FilterOrderUpdateList(ncolumn)
        elif colnumber == 0:
            column.sort()
            if not self.OnFilterHide:
                new_column = self.filter.compare_to_list(column)
                self.FilterOrderUpdateList(new_column)        
            else:
                self.FilterOrderUpdateList(column)
        else:
            column.sort()
            if not self.OnFilterHide:
                new_column = self.filter.sift_files(column, self.list_cache.get_all_files(), colnumber)
                self.FilterOrderUpdateList(new_column)
            else:
                self.FilterOrderUpdateList(column)

    def SortDates(self, column):
        """This function sorts the dates in the date column"""
        ndate = []
        datecontrol = []
        # Convert to time for easy formatting
        for col in column:
            count = col.count(':')
            if not count:
                ndate.append(time.strptime(col, '%b %d %Y'))
            else:
                new = col.rstrip() + ' 2007'
                if count == 3:
                    ndate.append(time.strptime(new, '%b %d %H:%M:%S %Y'))
                else:
                    ndate.append(time.strptime(new, '%b %d %H:%M %Y'))
                
        ndate.sort()
        n2date = []
        # Reconvert to string after sorting the dates
        for date in ndate:
            if not date[4]: 
                n2date.append(time.strftime('%b %d %Y', date))
            else:
                if date[5]:
                    n2date.append(time.strftime('%b %d %H:%M:%S', date))
                else:
                    n2date.append(time.strftime('%b %d %H:%M', date))

        return n2date
                
    def UpdateListCwdToThread(self, which_thread,  istr):
        """This function updates the cwd in the FTPThread, when the input is from the ListCtrl"""
        which_thread.update_cwd(istr)
        which_thread.cwd_Event.set()
        which_thread.set_activity(True)
        self.browse_ftp.SetValue(istr)

    def OnListRightDown(self, event):
        """This function will be called upon a right-click on the list-ctrl. The function
        creates a popup menu, which has one child(transfer). Using the child, you
        can transfer the file or directory."""
        if not hasattr(self, 'TransferID'):
            self.TransferID = wx.NewId()
            self.SendID = wx.NewId()
            self.DSendID = wx.NewId()
            self.Bind(wx.EVT_MENU, self.OnListTransfer, id = self.TransferID)
            self.Bind(wx.EVT_MENU, self.OnListFileSend, id = self.SendID)
            self.Bind(wx.EVT_MENU, self.OnListDirSend, id = self.DSendID)
            
        # Create a Menu and add event
        menu = wx.Menu()
        menu.Append(self.TransferID, 'Transfer')
        menu.Append(self.SendID, 'Send File')
        menu.Append(self.DSendID, 'Send Directory')
    
        # Create the popup menu and then destroy it
        self.PopupMenu(menu)
        menu.Destroy()

    def OnListTransfer(self, event):
        """This function is called when the transfer pop menu is clicked. Will transfer
        fill or directory."""
        self.size = self.GetSize()
        # Create transfer frame as needed
        self.DisplayTransferFrame()
        thread = self.FindRelevantThread()
        if thread:
            thread.update_tfile(self.currentFile)
            thread.set_filter(self.filter)
            thread.send_Event.set()
            thread.set_activity(True)

    def FindRelevantThread(self, selection = ''):
        """This function searches through all of the threads in the wx pool
        searching for the active one."""
        which_thread = None
        if not selection:
            selection = self.browse_ftp.GetValue()
        for thread in self.threads:
            try:
                root = thread.get_root()
                if selection.find(root) != -1:
                    which_thread = thread
                    break
            except:
                continue
        return which_thread

    def FinishDownload(self, fthread):
        """Update status of frames after up/download"""
        fthread.set_activity(False)

    def ShowTransferFrame(self, event):
        """Displays the transfer frame."""
        pass

    def ShowStatusFrame(self, event):
        """Displays the status frame"""
        pass

    def OnHide(self, event):
        """This function toggles the OnFilterHide flag"""
        if self.OnFilterHide:
            self.OnFilterHide = False
            if not self.Order:
                new_files = self.filter.compare_to_list(self.GetListOfFilenames())
                self.FilterUpdateList(newfiles)
            else:
                self.SortColumnAndUpdate(self.LastColumn)
        else:
            self.OnFilterHide = True
            if not self.Order:
                self.FilterUpdateList(self.GetListOfFilenames())
            else:
                self.SortColumnAndUpdate(self.LastColumn)
                
    def OnListDirSend(self, event):
        """This function sends a file to the FTP server."""
        self.dir_path = ''
        self.is_dir = True
        dialog = wx.DirDialog(self, 'Choose a directory:',
                              defaultPath = os.getcwd(),
                              style = wx.DD_DEFAULT_STYLE | wx.DD_NEW_DIR_BUTTON)
        if dialog.ShowModal() == wx.ID_OK:
            self.dir_path = dialog.GetPath()
        dialog.Destroy()
        self.InitClientTransfer()

    def OnListFileSend(self, event):
        """This function sends a directory to the FTP server."""
        self.dir_path = ''
        self.is_dir = False
        dialog = wx.FileDialog(self, 'Send a file...', os.getcwd(), style = wx.FD_OPEN)
        if dialog.ShowModal() == wx.ID_OK:
            self.dir_path = dialog.GetPath()
        dialog.Destroy()

        # Check for valid path
        if self.dir_path:
            self.DisplayTransferFrame()
            thread = self.FindRelevantThread()
            if thread:
                thread.update_sfile(self.dir_path, self.is_dir)
                thread.set_filter(self.filter)
                thread.transfer_Event.set()
                thread.set_activity(True)

    def DisplayTransferFrame(self):
        """Displays the transfer frame if created, or creates it and shows it"""
        pass

    def OpenUserPassFrame(self, thread):
        """This function opens the user pass frame for login. Takes in the parent thread"""
        self.userpass_frame = UserPassFrame(self, thread)
        self.userpass_frame.Show()

    def InitClientTransfer(self):
        """This function checks the path inputted by the user, and if it points to a file or
        directory activates the current thread to transfer the file to that directory."""
        # Check for valid path
        if self.dir_path:
            self.DisplayTransferFrame()
            thread = self.FindRelevantThread()
            if thread:
                thread.update_sfile(self.dir_path, self.is_dir)
                thread.set_filter(self.filter)
                thread.transfer_Event.set()
                thread.set_activity(True)

    def UpdateAfterDelete(self, thread):
        """This function updates the listctrl which virtualizes the FTP server."""
        if thread.complete_delete:
            self.list_cache.remove_file(thread.delete_file)
            self.new_list = self.GetListOfFilenames()
            if self.Order:
                self.FilterUpdateList(self.new_list)
            else:
                self.SortColumnAndUpdate(self.LastColumn)

    def GetListOfFilenames(self):
        """This function returns the list of filenames in ftp_tree"""
        return self.list_cache.get_filenames()

    def FilterUpdateList(self, new_list):
        """This function grabs the new_list, deletes the old items in the
        ListCtrl, and grabs the data from the cache for the new list"""
        ilist = self.list_cache.get_filter_files(new_list)
        self.ftp_tree.DeleteAllItems()
        for data in ilist:
            self.ftp_tree.Append(data)

    def FilterOrderUpdateList(self, new_list):
        """This function grabs an ordered list, compares it to the filter and then displays it"""
        ilist = self.list_cache.get_order_files(new_list, self.LastColumn)
        self.ftp_tree.DeleteAllItems()
        for data in ilist:
            self.ftp_tree.Append(data)

    def RemoveThread(self, thread):
        """This function removes the thread, and removes the thread from the cache"""
        # Only called from status frame
        self.threads.remove(thread)
        self.list_cache.clear()
        self.FilterUpdateList(self.GetListOfFilenames())
        self.browse_ftp.SetValue('')

    def SwitchThreads(self, threadname):
        """Function switches between threads."""
        thread = self.FindRelevantThread(threadname)
        self.ParseFTP(thread)

    #### Message Passing Functions
    #### Functions used to update for status updates to keep the user informed. wx.CallAfter
    #### used to instantiate the GUI thread, and Post an Event to the wx thread.
    def InitializeThread(self, selection, msg):
        """Initialize the visualization of a new thread."""
        self.status_notebook.ChangeSelection(1)
        self.status_frame.InitializeThread(selection, msg)

    def UpdateStatus(self, selection, msg):
        """Update the status of the selection's thread."""
        self.status_notebook.ChangeSelection(1)
        self.status_frame.UpdateStatus(selection, msg)

    def QueryUser(self, msg):
        """Query the user for input for ftp cancellation"""
        self.status_notebook.ChangeSelection(1)
        self.status_frame.QueryUser(msg)
    
class FTPApp(wx.App):
    """FTP application"""
    def OnInit(self):
        """Upon initialization of the application"""
        frame = FTPFrame()
        frame.Show()
        return True
    
def main():
    """Runs the FTP client program"""
    app = FTPApp(redirect = True, filename = 'debug.txt')
    app.MainLoop()
    
if __name__ == '__main__':
    main()
